/*
 * Copyright (C) 2008-2011 CBDP Consortium
 * 
 * All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.
 * 
 * This software consists of contributions made by many individuals, 
 * listed below:
 *
 * Author: 	Xabier Laiseca <xabier.laiseca@deusto.es>
 * 			David Ausín <david.ausin@deusto.es>
 */
package cbdp.server.contextreasoner.impl.reasoner;

import static cbdp.server.contextreasoner.impl.util.DebugUtils.*;

import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import cbdp.server.contextreasoner.Reasoner;
import cbdp.server.contextreasoner.exceptions.ReasonerException;
import cbdp.server.contextreasoner.internal.OntologyContainer;

import com.hp.hpl.jena.ontology.Individual;
import com.hp.hpl.jena.ontology.OntClass;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.ontology.OntModelSpec;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelChangedListener;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.shared.Lock;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
import com.hp.hpl.jena.vocabulary.OWL;

/**
 * @author  Xabier Laiseca (University of Deusto)
 */
public abstract class GenericReasoner implements Reasoner, ModelChangedListener
{
	private final static String NOT_EXISTING_RULE_SET_EXCEPTION_MESSAGE = "The rule set does not exist.";

	private final static String RDF_TYPE_PREDICATE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";

	private OntologyContainer ontologyContainer;

	private boolean synchronous = Boolean.getBoolean("cbdp.reasoner.synchronous");;
	private RuleScheduler scheduler;

	/* ********************* Methods to initialize the service ********************* */

	public void setOntologyContainer(final OntologyContainer ontologyContainer)
	{
		this.ontologyContainer = ontologyContainer;
	}
	
	private OntModel getOntModel()
	{
		return ontologyContainer.getOntModel();
	}
	
	public void load()
	{
		getOntModel().register(this);

		if(!synchronous)
		{
			loadScheduler();
		}
	}

	protected void loadScheduler()
	{
		scheduler = new RuleScheduler();
		final Thread schedulerThread = new Thread(scheduler);
		schedulerThread.start();
	}

	/* ********************* Methods to finalize the service ********************* */

	public void unload()
	{
		if (scheduler != null)
		{
			scheduler.finalizeRunnable();
			scheduler = null;
		}
	}

	/* ********************* Reasoner interface methods ********************* */

	@Override
	public String createRuleSet(final InputStream rules, final Map<String, Collection<String>> filter) throws ReasonerException
	{
		final RuleSet ruleSet = createRuleSet(rules);
		final String ruleSetId = generateReasonerId();
		addRuleSet(ruleSet, ruleSetId, filter);

		return ruleSetId;
	}
	
	public void startRuleSet(final String ruleSetName)
	{
		if(ruleSetInfosById.containsKey(ruleSetName))
		{
			final RuleSetInfo ruleSetInfo = ruleSetInfosById.get(ruleSetName);
			ruleSetInfo.start();
		}
	}
	
	public void stopRuleSet(final String ruleSetName)
	{
		if(ruleSetInfosById.containsKey(ruleSetName))
		{
			final RuleSetInfo ruleSetInfo = ruleSetInfosById.get(ruleSetName);
			ruleSetInfo.stop();
		}
	}

	@Override
	public Collection<String> getRuleSetNames()
	{
		return ruleSetInfosById.keySet();
	}

	@Override
	public void destroyRuleSet(final String ruleSetName) throws ReasonerException
	{
		if (ruleSetInfosById.containsKey(ruleSetName))
		{
			final RuleSet ruleSet = ruleSetInfosById.remove(ruleSetName).ruleSet;
			ruleSet.destroy();
		}
		else throw new ReasonerException(NOT_EXISTING_RULE_SET_EXCEPTION_MESSAGE);
	}

	@Override
	public void forceRuleSetLaunching(final String ruleSetName)	throws ReasonerException
	{
		boolean ontologyChanged = false;
		
		do
		{
			if (ruleSetInfosById.containsKey(ruleSetName))
			{
				final RuleSet ruleSet = ruleSetInfosById.get(ruleSetName).ruleSet;
				ontologyChanged = ruleSet.launch();
			}
		}
		while(ontologyChanged);
	}

	@Override
	public void launchRuleSets()
	{
		if(scheduler == null)
		{
			boolean ontologyChanged;
			
			do
			{
				ontologyChanged = false;
				
				final Collection<RuleSet> ruleSets = getRuleSetsToLaunch();
	
				for (final RuleSet ruleSet: ruleSets)
				{
					if(ruleSet.launch())
					{
						ontologyChanged = true;
					}
				}
			}
			while(ontologyChanged);
		}
		else
		{
			final RuleSetEnqueuer enqueuer = new RuleSetEnqueuer();
			final Thread enqueuerThread = new Thread(enqueuer);
			enqueuerThread.start();
		}
	}
	
	private class RuleSetEnqueuer implements Runnable
	{

		@Override
		public void run()
		{
			final Collection<RuleSet> ruleSets = getRuleSetsToLaunch();

			for (final RuleSet ruleSet: ruleSets)
			{
				scheduler.putInQueue(ruleSet);
			}
		}	
	}

	/* ************************************** Protected abstract methods ************************************* */

	protected abstract RuleSet createEmptyRuleSet();
	public abstract OntModelSpec getOntModelSpec();

	/* ************************************** Private methods ************************************** */

	private HashMap<String,RuleSetInfo> ruleSetInfosById = new HashMap<String, GenericReasoner.RuleSetInfo>();
	
	private class RuleSetInfo
	{
		private String configuredFilter;
		private String realFilter;
		
		private RuleSet ruleSet;
		
		private boolean stopped = false;
		
		private synchronized boolean isStopped()
		{
			return stopped;
		}
		
		private synchronized void start()
		{
			stopped = false;
		}
		
		private synchronized void stop()
		{
			stopped = true;
		}
	}
	
	private void addRuleSet(final RuleSet ruleSet, final String ruleSetId, final Map<String, Collection<String>> configuradFilter)
	{
		final RuleSetInfo ruleSetInfo = new RuleSetInfo();
		
		ruleSetInfo.configuredFilter = createFilter(configuradFilter.keySet());
		ruleSetInfo.ruleSet = ruleSet;
		
		final HashSet<String> tempFilter = new HashSet<String>();
		
		for(final String filterOntClass: configuradFilter.keySet())
		{
			tempFilter.add(filterOntClass);
				
			for(final String superclass: getSubClassesNames(filterOntClass))
			{
				tempFilter.add(superclass);
			}
		}
		
		ruleSetInfo.realFilter = createFilter(tempFilter);
		
		ruleSetInfosById.put(ruleSetId, ruleSetInfo);
	}
	
	private Collection<String> getSubClassesNames(final String ontClassName)
	{
		getOntModel().enterCriticalSection(Lock.READ);
		
		try
		{
			final OntClass ontClass = getOntModel().getOntClass(ontClassName);
		
			return getSubClassesNames(ontClass);
		}
		finally
		{
			getOntModel().leaveCriticalSection();
		}
	}
	
	private Collection<String> getSubClassesNames(final OntClass ontClass)
	{
		//final ExtendedIterator<OntClass> subclassIt = ontClass.listSubClasses(false);
		final ExtendedIterator<OntClass> subclassIt = ontClass.listSubClasses();
		
		final Collection<String> subclasses = new HashSet<String>();
		
		while (subclassIt.hasNext())
		{
			final OntClass subclass = subclassIt.next();
			
			if(!subclass.isAnon())
			{
				subclasses.add(subclass.getURI());
			}
			
			/*for(final String subsubclass: getSubClassesNames(subclass))
			{
				if(!subclasses.contains(subsubclass))
				{
					subclasses.add(subsubclass);
				}
			}*/
		}
		
		return subclasses;
	}

	private String createFilter(final Collection<String> classes)
	{
		if (classes.size() > 0)
		{
			final StringBuilder builder = new StringBuilder();
			
			for(final String clazz: classes)
			{
				builder.append(clazz);
				builder.append(" ");
			}
	
			builder.deleteCharAt(builder.length() - 1);
			
			return builder.toString();
		}
		else return "";
	}

	private volatile static int reasonerNextId = 1;
	private static Object reasonerNextIdtLock = new Object();

	private String generateReasonerId()
	{
		int tempReasonerNextId;

		synchronized (reasonerNextIdtLock)
		{
			tempReasonerNextId = reasonerNextId;
			reasonerNextId++;
		}

		return "Reasoner_" + tempReasonerNextId;
	}

	private RuleSet createRuleSet(final InputStream rules) throws ReasonerException
	{
		final RuleSet ruleSet = createEmptyRuleSet();
		ruleSet.setOntModel(getOntModel());
		ruleSet.setRules(rules);
		ruleSet.load();

		return ruleSet;
	}

	private Collection<RuleSet> getRuleSetsToLaunch()
	{
		final Vector<RuleSet> ruleSetsToLaunch = new Vector<RuleSet>();

		if ((modifiedClasses.size() > 0) && (ruleSetInfosById.size() > 0))
		{
			final Map<String, Collection<String>> tempModifiedClasses = new HashMap<String, Collection<String>>();

			synchronized (modifiedClasses)
			{
				tempModifiedClasses.putAll(modifiedClasses);
				modifiedClasses.clear();
			}
			
			final String regex = createFilterRegexString(tempModifiedClasses.keySet());
			final Pattern pattern = Pattern.compile(regex);

			for(final RuleSetInfo ruleSetInfo: ruleSetInfosById.values())
			{
				if (!ruleSetInfo.isStopped())
				{
					final Matcher matcher = pattern.matcher(ruleSetInfo.realFilter);
					
					if(matcher.find())
					{
						ruleSetsToLaunch.add(ruleSetInfo.ruleSet);
					}
				}
			}
		}

		return ruleSetsToLaunch;
	}
	
	private void addClassFilterToStringBuilder(final String clazz, final StringBuilder builder)
	{
		builder.append("^(.* )?");
		builder.append(clazz);
		builder.append("( .*)?$");
	}
	
	private String createFilterRegexString(final String clazz)
	{
		final StringBuilder builder = new StringBuilder();
		
		addClassFilterToStringBuilder(clazz, builder);
		
		return builder.toString();
	}
	
	private String createFilterRegexString(final Collection<String> classes)
	{
		final StringBuilder builder = new StringBuilder();
		
		for(final String clazz: classes)
		{
			addClassFilterToStringBuilder(clazz, builder);
			builder.append("|");
		}

		builder.deleteCharAt(builder.length()-1);
		
		return builder.toString();
	}

	private Collection<String> getSuperClassesNames(final String baseOntClassName)
	{
		try
		{
			getOntModel().enterCriticalSection(Lock.READ);
			
			final Vector<String> superClassesNames = new Vector<String>();
	
			final OntClass baseOntClass = getOntModel().getOntClass(baseOntClassName);
	
			if (baseOntClass != null)
			{
				final Collection<OntClass> superClasses = getSuperClasses(baseOntClass);
	
				for (final OntClass superClass : superClasses)
				{
					superClassesNames.add(superClass.getURI());
				}
			}
	
			return superClassesNames;
		}
		finally
		{
			getOntModel().leaveCriticalSection();
		}
	}

	private Collection<OntClass> getSuperClasses(final OntClass baseOntClass)
	{
		final Vector<OntClass> superClasses = new Vector<OntClass>();

		final ExtendedIterator<OntClass> directSuperClasses = baseOntClass.listSuperClasses(true);

		while (directSuperClasses.hasNext())
		{
			final OntClass superClass = directSuperClasses.next();
			final Collection<OntClass> tempSuperClasses = getSuperClasses(superClass);
			addAllIfNotContains(superClasses, tempSuperClasses);

			if (!superClass.isAnon())
			{
				addIfNotContains(superClasses, superClass);
			}
		}

		return superClasses;
	}

	private <T> void addAllIfNotContains(final Collection<T> collection, final Collection<T> elementsToAdd)
	{
		for (final T elementToAdd : elementsToAdd)
		{
			addIfNotContains(collection, elementToAdd);
		}
	}

	private <T> void addIfNotContains(final Collection<T> collection, final T element)
	{
		if (!collection.contains(element))
		{
			collection.add(element);
		}
	}

	private class RuleScheduler implements Runnable
	{
		private final LinkedBlockingQueue<RuleSet> queue = new LinkedBlockingQueue<RuleSet>();
		private volatile boolean finished = false;

		private void putInQueue(final RuleSet ruleSetName)
		{
			if (!queue.contains(ruleSetName))
			{
				try 
				{
					queue.put(ruleSetName);
				}
				catch (final InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		}

		@Override
		public void run()
		{
			while (!finished)
			{
				try
				{
					final RuleSet ruleSet = queue.take();
					
					if(ruleSet.launch())
					{
						for(final RuleSet newRuleSet: getRuleSetsToLaunch())
						{
							putInQueue(newRuleSet);
						}
					}
				}
				catch (final InterruptedException e)
				{

					e.printStackTrace();
				}
			}
		}

		private void finalizeRunnable()
		{
			finished = true;
		}
	}

	/* ******************************************** ModelChangedListener methods ******************************************* */

	private Map<String, Collection<String>> modifiedClasses = Collections.synchronizedMap(new HashMap<String, Collection<String>>());

	@Override
	public void addedStatement(final Statement statement)
	{
		processStatement(statement);
	}

	private <T, X> void putIfNotContains(final Map<T, Collection<X>> map, final T key, final X value)
	{
		if (!map.containsKey(key))
		{
			map.put(key, new Vector<X>());
		}

		if (value != null)
		{
			final Collection<X> values = map.get(key);

			if (!values.contains(value))
			{
				values.add(value);
			}
		}
	}

	@Override
	public void addedStatements(final Statement[] statements)
	{
		for (final Statement statement : statements)
		{
			processStatement(statement);
		}
	}

	@Override
	public void addedStatements(final List<Statement> statements)
	{
		addedStatements(statements.toArray(new Statement[statements.size()]));
	}

	@Override
	public void addedStatements(final StmtIterator statements)
	{
		addedStatements(statements.toList());
	}

	@Override
	public void addedStatements(final Model model)
	{
		addedStatements(model.listStatements());
	}

	private void processStatement(final Statement statement)
	{
		// When a new individual is added to the ontology
		if (statement.getPredicate().getURI().equals(RDF_TYPE_PREDICATE))
		{
			if(statement.getObject().isURIResource() && statement.getObject().as(Resource.class).getURI().equals(OWL.Class.getURI()))
			{
				updateRuleSetFilters(statement.getSubject().getURI());
			}
			
			putIfNotContains(modifiedClasses, statement.getObject().as(Resource.class).getURI(), null);
		}
		// When an individual is modified
		else if (statement.getSubject().canAs(Individual.class))
		{
			final Individual individual = statement.getSubject().as(Individual.class);
			putIfNotContains(modifiedClasses, individual.getOntClass().getURI(), statement.getPredicate().getURI());
		}
		else printStatementsIfInDebugMode(statement);
	}
	
	private void updateRuleSetFilters(final String newClass)
	{
		final Pattern patternForRealFilter = Pattern.compile( createFilterRegexString(newClass) );
		
		final Collection<String> classes = getSuperClassesNames(newClass);
		classes.add(newClass);
		final Pattern patternForConfiguredFilter = Pattern.compile( createFilterRegexString(classes) );
		
		for(final RuleSetInfo ruleSetInfo: ruleSetInfosById.values())
		{
			if(!patternForRealFilter.matcher(ruleSetInfo.realFilter).matches() 
					&& patternForConfiguredFilter.matcher(ruleSetInfo.configuredFilter).matches())
			{				
				updateRuleSetFilter(ruleSetInfo, newClass);
			}
		}
	}
	
	

	private void updateRuleSetFilter(final RuleSetInfo ruleSetInfo, final String newClass)
	{
		ruleSetInfo.realFilter += " " + newClass; 
	}

	@Override
	public void notifyEvent(final Model model, final Object event)
	{
		printlnIfInDebugModel("notifyEvent(final Model model, final Object event)");
		printlnIfInDebugModel("Event: " + event);
		printStatementsIfInDebugMode(toArray(model));
	}

	@Override
	public void removedStatement(final Statement statement)
	{
		printlnIfInDebugModel("removedStatement(final Statement statement)");
		printStatementsIfInDebugMode(statement);
	}

	@Override
	public void removedStatements(final Statement[] statements)
	{
		printlnIfInDebugModel("removedStatements(final Statement[] statements)");
		printStatementsIfInDebugMode(statements);
	}

	@Override
	public void removedStatements(final List<Statement> statements)
	{
		printlnIfInDebugModel("removedStatements(final List<Statement> statements)");
		printStatementsIfInDebugMode(toArray(statements));
	}

	@Override
	public void removedStatements(final StmtIterator statements)
	{
		printlnIfInDebugModel("removedStatements(final StmtIterator statements)");
		printStatementsIfInDebugMode(toArray(statements));
	}

	@Override
	public void removedStatements(final Model model)
	{
		printlnIfInDebugModel("removedStatements(final Model model)");
		printStatementsIfInDebugMode(toArray(model));
	}

	private Statement[] toArray(final Collection<Statement> statements)
	{
		return statements.toArray(new Statement[statements.size()]);
	}

	private Statement[] toArray(final StmtIterator statements)
	{
		return toArray(statements.toList());
	}

	private Statement[] toArray(final Model model)
	{
		return toArray(model.listStatements());
	}

	private void printStatementsIfInDebugMode(final Statement... statements)
	{
		if (isInDebugMode())
		{
			println();
	
			final StringBuilder builder = new StringBuilder();
			
			for (final Statement statement : statements)
			{
				builder.append("Statement: ");
				builder.append(statement.toString());
				builder.append("\n");
			}
			
			print(builder.toString());
	
			println();
			println();
		}
	}

}
